/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Canadian Weather.
 *
 * The Initial Developer of the Original Code is
 *   George Bradt.
 *
 * Portions created by the Initial Developer are Copyright (C) 2011
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   George Bradt.
 *
 * Credits:
 *   Sun icons from Icons-Land at http://www.icons-land.com/vista-weather-icons.php
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

// Import the APIs we need.
const {Cc, Ci} = require('chrome');
const widgets = require('sdk/widget');
const panels = require('sdk/panel');
const data = require('sdk/self').data;
const simpleStorage = require('sdk/simple-storage');
const timers = require('sdk/timers');
const requests = require('sdk/request');
const notify = require('notify');
const textToXml = require('textToXml');
const parseXml = require('parseXml');

// To convert EC radar images page (string) to Document.
const parser = Cc["@mozilla.org/xmlextras/domparser;1"].
        createInstance(Ci.nsIDOMParser);

var CanWeather = {
    // My widget and panels.
    myWidget: null,
    optionsPanel: null,
    conditionsPanel: null,
    warningPanel: null,
    errorPanel: null,
    
    // Miscellaneous.
    timerId: 0,                    // setInterval id
    errorMessage: '',              // message to error panel
    retryOnNetworkError: true,     // retry data file request on a network error
                                   // (status code = 0)
    combinedData: '',              // includes all current warnings - to
                                   // conditions panel (which always shows all
                                   // current warnings)
    currentWarningsToShow: [],     // warnings to display in warnings panel
    previousWarnings: [],          // warnings from previous fetch
    radarImageRainList: [],        // data scraped from EC radar images page
    radarImageSnowList: [],        // data scraped from EC radar images page

    // This should only run at install.
    initializeOptions: function() {
        //console.log('Canadian Weather: initializing options');

        simpleStorage.storage.options = {
            language: 'e',  // e=English, f=French
            location: {
                provinceIndex: 0,
                provinceCode: '',
                siteIndex: 0,
                siteCode: ''
            },
            frequencyMilli: 3600000,
            toolbarDisplay: {
                showTemperature: true,
                showWind: false,
                showWindChill: false,
                showHumidity: false,
                showHumidex: false,
                showCondition: false,
                showConditionIcon: false
            },
            forecastInToolbar: {
                showToday: false,
                showTonight: false,
                showTodayPlus1: false,
                showTodayPlus2: false,
                showDayLabel: false,
                showForecastTemperature: false,
                showPop: false,
                showForecastIcon: false
            },
            toolbarStyle: {
                bold: false,
                italic: false,
                fontsizeIndex: 0,
                color: {
                    normal: ['', ''],
                    priorityLow: ['', ''],
                    priorityMedium: ['', ''],
                    priorityHigh: ['', ''],
                    priorityUrgent: ['', '']
                }
            },
            radar: {
                provinceIndex: 0,
                stationIndex: 0,
                stationName: '',
                stationCode: ''
            },
            radarEnabled: false,
            toolbarDisplayEnabled: true,    // text portion
            forecastInToolbarEnabled: false,
            toolbarColorEnabled: false,
            conditionsPanelEnabled: false,
            warningPanelEnabled: true,
            showWarningOnlyOnce: true
        };
    },

    // This widget displays images and/or weather data of variable length,
    // in the bar.
    createWidget: function() {
        var message, priority, warningsPanel;
        
        CanWeather.myWidget = widgets.Widget({
        id: 'canweather',
        label: 'Canadian Weather',
        width: 32,
        contentURL: data.url('widget/widget.html'),
        contentScriptWhen: 'end',
        contentScriptFile: data.url('widget/widget.js')
        });

        // Handlers for user-defined events emitted from
        // content script widget.js.
        
        // On init, and if a new browser window opened.
        CanWeather.myWidget.port.on('newWindow', function() {
            CanWeather.myWidget.port.emit('buildToolbar',
                                          simpleStorage.storage.options,
                                          CanWeather.combinedData);
        });

        // Set the widget width.
        CanWeather.myWidget.port.on('width', function(aWidth) {
            CanWeather.myWidget.width = aWidth;
        });

        // Valid weather warning priorities are
        // 'Low', 'Medium', 'High' and 'Urgent'.
        CanWeather.myWidget.port.on('unexpectedPriorityError',
                                    function(aUnexpectedPriority) {
            message = 'A warning has an unexpected priority value: ' +
                      aUnexpectedPriority;
            priority = 'medium';
            warningsPanel = null;
            notify.displayNotification(message,
                                       priority,
                                       warningsPanel);
        });

        CanWeather.myWidget.port.on('right-click', function() {
            CanWeather.optionsPanel.show();
        });

        CanWeather.myWidget.port.on('left-click', function(aRadarClicked) {
            if ( aRadarClicked ) {
                // User clicked on radar image.
                CanWeather.fetchRadarPage();    
            }
            else if ( simpleStorage.storage.options.conditionsPanelEnabled ) {
                if ( CanWeather.combinedData ) {
                    CanWeather.conditionsPanel.show();
                }
                else {
                    message = 'There is no data to show yet';
                    priority = 'low';
                    warningsPanel = null;
                    notify.displayNotification(message,
                                               priority,
                                               warningsPanel);
                }
            }
        });
        
        // User hovered mouse over toolbar display.
        CanWeather.myWidget.port.on('setTooltip', function(aTooltipText) {
            CanWeather.myWidget.tooltip = aTooltipText;
        });
    },

    // End widget handlers.

    // We have five panels.
    createPanels: function() {
        // Options panel will show when right-clicking the widget.
        CanWeather.optionsPanel = panels.Panel({
            width: 790,
            height: 510,
            contentURL: data.url('options/options.html'),
            contentScriptFile: data.url('options/options.js'),
            contentScriptWhen: 'end',
            onShow: function() {
                // Send options to content script options.js.
                this.port.emit('show', simpleStorage.storage.options);
            }
        });

        // Handlers for user-defined events emitted from
        // content script options.js.
        
        // If something has gone wrong with the toolbar display, attempt to fix.
        // User clicked the Rebuild button in options panel.
        CanWeather.optionsPanel.port.on('rebuild-click', function() {
            CanWeather.myWidget.port.emit('buildToolbar',
                                          simpleStorage.storage.options,
                                          CanWeather.combinedData);
        });

        // User clicked the Ok button in options panel.
        // This requests weather data from EC for the chosen site.
        CanWeather.optionsPanel.port.on('ok-click', function(aOptions) {
            var previousFrequency = simpleStorage.storage.options.frequencyMilli;
            var newFrequency = aOptions.frequencyMilli;
            simpleStorage.storage.options = aOptions;
            CanWeather.optionsPanel.hide();
            
            if ( simpleStorage.storage.options.location.siteCode ) {
                CanWeather.fetchWeatherData();

                if ( !CanWeather.timerId ) {
                    CanWeather.timerId = timers.setInterval(
                            function() { CanWeather.fetchWeatherData(); },                                         newFrequency);
                }
                else if ( newFrequency != previousFrequency ) {
                    timers.clearInterval(CanWeather.timerId);
                    CanWeather.timerId = timers.setInterval(
                            function() { CanWeather.fetchWeatherData(); },                                         newFrequency);
                }
            }
        });

        // User clicked the Cancel button in options panel.
        CanWeather.optionsPanel.port.on('cancel-click', function() {
            CanWeather.errorPanel.hide();
            CanWeather.optionsPanel.hide();
        });

        // User clicked the Ok button in options panel,
        // but an error was detected.
        // An error message is displayed in the error panel,
        // and the options panel remains shown.
        CanWeather.optionsPanel.port.on('error', function(aErrorMessage) {
            console.log('Canadian Weather: ' + aErrorMessage);
            CanWeather.errorMessage = aErrorMessage;
            CanWeather.errorPanel.show();
        });

        // End options handlers.

        // Radar panel will show when left-clicking the widget radar image.
        CanWeather.radarPanel = panels.Panel({
            width: 800,
            height: 650,
            contentURL: data.url('radar/radar.html'),
            contentScriptFile: data.url('radar/radar.js'),
            contentScriptWhen: 'end',
            onShow: function() {
                // Pass the scraped image lists, radar options.
                this.port.emit('show',
                               CanWeather.radarImageRainList,
                               CanWeather.radarImageSnowList,
                               simpleStorage.storage.options.radar);
            }
        });

        // Handlers for user-defined events emitted from content script radar.js.
        
        // User clicked the Ok button in radar panel.
        CanWeather.radarPanel.port.on('ok-click', function() {
            CanWeather.radarPanel.hide();
        });

        // End radar handlers.

        // Conditions panel will show when left-clicking the widget,
        // if it has been enabled in options.
        CanWeather.conditionsPanel = panels.Panel({
            width: 750,
            height: 450,
            contentURL: data.url('conditions/conditions.html'),
            contentScriptFile: data.url('conditions/conditions.js'),
            contentScriptWhen: 'end',
            onShow: function() {
                // Send the current conditions to content script conditions.js.
                this.port.emit('show', CanWeather.combinedData);
            }
        });

        // Handlers for user-defined events emitted from
        // content script conditions.js.
        
        // User clicked the Ok button in conditions panel.
        CanWeather.conditionsPanel.port.on('ok-click', function() {
            CanWeather.conditionsPanel.hide();
        });

        // End conditions handlers.

        // Warnings panel will show when a weather alert is issued
        // for the tracked location.
        // User clicked the Details button in notification box.
        CanWeather.warningPanel = panels.Panel({
            width: 500,
            height: 320,
            contentURL: data.url('warning/warning.html'),
            contentScriptFile: data.url('warning/warning.js'),
            contentScriptWhen: 'end',
            onShow: function() {
                // Send the new warnings to content script warning.js.
                this.port.emit('show', CanWeather.currentWarningsToShow);
            }
        });

        // Handlers for user-defined events emitted from
        // content script warning.js.
        
        // User clicked the Ok button in warning panel.
        CanWeather.warningPanel.port.on('ok-click', function() {
            CanWeather.warningPanel.hide();
        });

        // End warning handlers.
        
        // Error panel will show when a content script detects an error.
        CanWeather.errorPanel = panels.Panel({
            width: 200,
            height: 140,
            contentURL: data.url('error/error.html'),
            contentScriptFile: data.url('error/error.js'),
            contentScriptWhen: 'end',
            onShow: function() {
                // Send the error message to content script error.js.
                this.port.emit('show', CanWeather.errorMessage);
            }
        });
        
        // Handlers for user-defined events emitted from content script error.js.

        // User clicked the Ok button in error panel.
        CanWeather.errorPanel.port.on('ok-click', function() {
            CanWeather.errorPanel.hide();
        });

        // End error handlers.
    },

    // Request Environment Canada radar page for chosen radar station.
    fetchRadarPage: function() {
        var message, priority, warningsPanel;

        // Make sure user has selected a radar station.
        if ( simpleStorage.storage.options.radarEnabled ||
             simpleStorage.storage.options.radar.stationCode ) {
            var radarPageRequest = requests.Request({
                url: 'http://dd.weatheroffice.ec.gc.ca/radar/PRECIPET/GIF/' +
                     simpleStorage.storage.options.radar.stationCode + '/',
                onComplete: function (response) {
                    if ( response.statusText == 'OK' ) {
                        CanWeather.xhrSuccessRadar(response.text);
                    }
                    else {
                        // Using the same function for any request,
                        // so pass caller id.
                        var caller = 'fetchRadarPage';
                        message = 'Radar images could not be retrieved. ' +
                                  'Status code: ';
                        CanWeather.xhrFail(response.status, message, caller);
                    }
                }
            }).get();
        }
        else {
            // Should never reach here.
            message = 'Please enable radar and select a station';
            priority = 'low';
            warningsPanel = null;
            notify.displayNotification(message,
                                       priority,
                                       warningsPanel);
        }
    },

    // Scrape radar page for radar images.
    xhrSuccessRadar: function(aResponse) {
        try {
            var message, priority, warningsPanel;
            
            // Create a Document.
            var doc = parser.parseFromString(aResponse, "text/html");
            
            // The links property returns a collection of all anchor elements
            // in a document with a value for the href attribute.
            var allLinks = doc.links;
            
            if ( allLinks.length < 28 ) {
                throw new Error('Unexpected result searching radar image list');
            }
            else {
                var radarImageRainList = [];
                var radarImageSnowList = [];
                var anHour = 60 * 60 * 1000;
                var prevImageTime = 0;
                var hrefLowerCase, currImageTime, isRainImage, isSnowImage;
                
                // Typically, a station will generate two 'rain' and two 'snow'
                // images every 10 minutes. These include two 'a11y' enhanced
                // images for visually impaired, which use a different colour 
                // scheme. So the last 28 images cover the last hour.
                // Used to animate the radar display.
                for ( let i = allLinks.length - 1; i >= allLinks.length - 28; i-- ) {
                    hrefLowerCase = allLinks[i].href.toLowerCase();
                    
                    if ( hrefLowerCase.lastIndexOf('a11y') > -1 ) {
                        // Bypass the 'a11y' images. They use white backgrounds.
                        continue;
                    }
                    
                    if ( hrefLowerCase.lastIndexOf('rain') > -1 ) {
                        isRainImage = true;
                        isSnowImage = false;
                    }
                    else if ( hrefLowerCase.lastIndexOf('snow') > -1 ) {
                        isSnowImage = true;
                        isRainImage = false;
                    }
                    else {
                        continue;
                    }
                    
                    // Large time gaps between images would make
                    // animation pointless.
                    currImageTime = new Date(allLinks[i].href.slice(0,12).replace(
                                             /^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)$/,
                                             '$4:$5 +0000 $2/$3/$1')).getTime();
                    
                    if ( prevImageTime == 0 ||
                         currImageTime - prevImageTime < anHour ) {
                        if ( isRainImage ) {
                            radarImageRainList.push(allLinks[i].href);
                        }
                        else {
                            radarImageSnowList.push(allLinks[i].href);
                        }
                        
                        prevImageTime = currImageTime;
                    }
                    else {
                        continue;    
                    }
                }
            
                if ( radarImageRainList.length > 0 ||
                     radarImageSnowList.length > 0 ) {
                    CanWeather.radarImageRainList = radarImageRainList.slice(0, 7);
                    CanWeather.radarImageSnowList = radarImageSnowList.slice(0, 7);
                    CanWeather.radarPanel.show();
                }
                else {
                    throw new Error('No radar images found');
                }
            }
        }
        catch(e) {
            message = e.name + ' line ' + e.lineNumber + ' --> ' + e.message;
            priority = 'high';
            warningsPanel = null;
            notify.displayNotification(message,
                                       priority,
                                       warningsPanel);
        }
    },

    // Request XML weather data file for the site chosen in options.
    fetchWeatherData: function() {
        var message, priority, warningsPanel;

        // Make sure user has set some display option
        // which requires data (ie. radar excluded).
        if ( simpleStorage.storage.options.toolbarDisplayEnabled ||
             simpleStorage.storage.options.toolbarDisplay.showConditionIcon ||
             simpleStorage.storage.options.conditionsPanelEnabled ||
             simpleStorage.storage.options.warningPanelEnabled ) {
            var prov = simpleStorage.storage.options.location.provinceCode;
            var site = simpleStorage.storage.options.location.siteCode;
            var language = simpleStorage.storage.options.language;

            var weatherDataRequest = requests.Request({
                url: 'http://dd.weatheroffice.ec.gc.ca/citypage_weather/xml/' +
                     prov + '/' + site + '_' + language + '.xml',
                onComplete: function (response) {
                    if ( response.statusText == 'OK' ) {
                        CanWeather.xhrSuccessWeather(response.text);
                    }
                    else {
                        var caller = 'fetchWeatherData';
                        message = 'Site data could not be retrieved. ' +
                                  'Status code: ';
                        CanWeather.xhrFail(response.status, message, caller);
                    }
                }
            }).get();
        }
        else {
            if ( !simpleStorage.storage.options.radarEnabled ) {
                message = 'You have no display options enabled';
                priority = 'low';
                warningsPanel = null;
                notify.displayNotification(message,
                                           priority,
                                           warningsPanel);
                console.log('Canadian Weather: ' + message);
            }
            
            CanWeather.myWidget.port.emit('buildToolbar',
                                          simpleStorage.storage.options,
                                          CanWeather.combinedData);
        }
    },

    // Site data retrieved.
    xhrSuccessWeather: function(aResponse) {
        CanWeather.retryOnNetworkError = true;

        // Due to bug 611042 (request.response.xml pulled from request
        // module api) we convert the response text to an XML object ourselves.
        var xml = new textToXml.convert(aResponse);
        //console.log('Canadian Weather: site data retrieved for ' +
        //    xml.rootElement.childElement('location').childElement('name').text +
        //    ' at ' + new Date().toLocaleTimeString());

        // Gather all of our data.
        CanWeather.combinedData = parseXml.parse(xml, simpleStorage.storage.options);
        
        CanWeather.myWidget.port.emit('buildToolbar',
                                      simpleStorage.storage.options,
                                      CanWeather.combinedData);
        CanWeather.manageWarnings();
    },

    // Do not show a previously shown warning
    // in the warning panel (if that option enabled).
    // All warnings are always shown in the conditions panel.
    manageWarnings: function() {
        var message, priority, warningsPanel;

        if ( simpleStorage.storage.options.warningPanelEnabled &&
             CanWeather.combinedData.warnings.length > 0 ) {
            // Do not touch CanWeather.currentWarningsToShow
            // (used by warnings panel) unless there are new warnings to show.
            // This is in case a notification box with warnings to show
            // is still open from the previous cycle.
            var currentWarningsToShow = [];
            
            if ( simpleStorage.storage.options.showWarningOnlyOnce ) {
                // url.
                currentWarningsToShow.push(CanWeather.combinedData.warnings[0]);
                
                var match;
                
                // Bypass first element in combinedData.warnings,
                // which is the url to EC warning details.
                for ( let i = 1; i < CanWeather.combinedData.warnings.length; i++ ) {
                    match = false;
                    
                    for ( let j = 0; j < CanWeather.previousWarnings.length; j++ ) {
                        // Check if this warning has already been displayed.
                        if ( CanWeather.combinedData.warnings[i].location ==
                                 CanWeather.previousWarnings[j].location &&
                             CanWeather.combinedData.warnings[i].type ==
                                 CanWeather.previousWarnings[j].type &&
                             CanWeather.combinedData.warnings[i].priority ==
                                 CanWeather.previousWarnings[j].priority &&
                             CanWeather.combinedData.warnings[i].description ==
                                 CanWeather.previousWarnings[j].description ) {
                            // Yes it has.
                            match = true;
                            break;
                        }
                    }

                    if ( !match ) {
                        // New one, show it.
                        currentWarningsToShow.push(CanWeather.combinedData.warnings[i]);
                    }
                }
                
                if ( currentWarningsToShow.length > 1 ) {
                    // New ones to show this cycle.
                    CanWeather.currentWarningsToShow = currentWarningsToShow;
                }
            }
            else {
                // Show them all. Make sure to update both arrays.
                currentWarningsToShow = CanWeather.combinedData.warnings;
                CanWeather.currentWarningsToShow = CanWeather.combinedData.warnings;
            }

            // Don't include the url element.
            CanWeather.previousWarnings = CanWeather.combinedData.warnings.slice(1);
            
            if ( currentWarningsToShow.length > 1 ) {
                message = CanWeather.combinedData.warnings[1].description;
                
                if ( currentWarningsToShow.length > 2 ) {
                    // Multiple warnings.
                    message += ' (and others)';
                }
                
                priority = ( CanWeather.combinedData.warnings[1].type == 'Warning' ) ?
                                'high' : 'medium';
                notify.displayNotification(message,
                                           priority,
                                           CanWeather.warningPanel);
            }
        }
        else {
            CanWeather.previousWarnings = [];    
        }
    },

    // Radar page or XML site data could not be retrieved.
    xhrFail: function(aStatusCode, aFailMessage, aCaller) {
        var message, priority, warningsPanel;

        if ( aStatusCode == '0' ) {
            // Network error.
            console.log('Canadian Weather: network error, ' +
                        'could not access Environment Canada server');

            if ( CanWeather.retryOnNetworkError ) {
                // Wait 5 minutes and retry the request.
                CanWeather.retryOnNetworkError = false;
                console.log('Canadian Weather: retrying...');
                
                if ( aCaller == 'fetchWeatherData' ) {
                        timers.setTimeout(function() {
                            CanWeather.fetchWeatherData(); },
                            300000);
                }
            }
            else {
                CanWeather.retryOnNetworkError = true;
                message = 'Network error! ' +
                          'Could not access Environment Canada server';
                priority = 'high';
                warningsPanel = null;
                notify.displayNotification(message,
                                           priority,
                                           warningsPanel);
            }
        }
        else {
            CanWeather.retryOnNetworkError = true;
            console.log('Canadian Weather: ' + aFailMessage + aStatusCode);
            message = aFailMessage + aStatusCode;
            priority = 'high';
            warningsPanel = null;
            notify.displayNotification(message,
                                       priority,
                                       warningsPanel);
        }
    },
};

// This function will be called when the add-on is loaded.
exports.main = function() {
    if ( !simpleStorage.storage.options ) {
        // This should only occur once.
        CanWeather.initializeOptions();
    }
    else if ( !simpleStorage.storage.options.toolbarStyle.fontsizeIndex ) {
        simpleStorage.storage.options.toolbarStyle.fontsizeIndex = 0;
    }
    else if ( !simpleStorage.storage.options.toolbarDisplay.showHumidity ) {
        // Added in version 1.3.0.
        simpleStorage.storage.options.toolbarDisplay.showHumidity = false;
    }

    CanWeather.createPanels();
    CanWeather.createWidget();

    if ( simpleStorage.storage.options.location.siteCode ) {
        CanWeather.fetchWeatherData();
        CanWeather.timerId = timers.setInterval(
                function() { CanWeather.fetchWeatherData(); },
                             simpleStorage.storage.options.frequencyMilli);
    }
};
